在 Low-Latency C++ 的設計中,最大的困難並非只有程式速度的提升,而是在高併發及高負載下仍然維持可預期且穩定的延遲。對金融交易、即時行情、遊戲引擎、網路服務、訊息佇列與高效能資料處理系統而言,P99 的 latency 往往才是決定維運品質的關鍵因素。
歷史悠久的 std::mutex 是最直觀的同步工具,但 mutex 的成本不僅僅是執行一次 lock/unlock 所需要的 CPU cycles。當競爭發生時,thread 可能被阻塞、觸發 context switch,甚至進入 kernel。這些行為會造成 latency spike,使原本幾微秒的操作突然變成數十甚至數百微秒。因此,在極端低延遲場景中,lock-free programming 成為一項重要技術。
1. Lock-Free 到底解決了什麼?
Lock-free 的核心概念,是透過 atomic operation 建立多執行緒間的同步,而不是讓 thread 互相等待 mutex。
傳統 mutex 不只是速度慢,其可能造成:
lock 的 thread 必須等待。lock。lock,高優先權 thread 反而被卡住。lock 使用不當可能互相等待。Thread AㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤThread B
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ ㅤ│
ㅤㅤ├── lock ───────┐ㅤ│
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │ㅤ│
ㅤㅤ│ㅤㅤ修改 shared data ㅤㅤ│ㅤ├── lock
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │ㅤ│
ㅤㅤ├── unlock ──────┘ㅤ│
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ ㅤ├── 被阻塞
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ ㅤ└── 等待 A
以下是一個最基本的例子。這個寫法利用 CPU 提供的 atomic instruction,不需要 mutex 就可以安全地讓多個 thread 同時修改 counter:
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);
lock(); // No need.
counter++; // No need.
unlock(); // No need.
當然,使用 std::atomic 並不代表程式自動變成高效能。真正重要的是理解 atomicity、memory ordering、cache coherence 與資料結構設計之間的關係。
上述例子也可以改成 memory_order_seq_cst。雖然語意簡單,但可能引入不必要的 ordering constraint。若這個 counter 只是統計事件數量,而其他 thread 並不依賴 counter 來判斷資料是否已經準備完成,那麼 memory_order_relaxed 通常已經足夠。
2. Lock-Free 到底保障了什麼?
低延遲程式設計的原則是:不要盲目使用最強的 synchronization guarantee,而應該選擇足夠正確的 memory ordering。而 lock-free 是 progress guarantee,即使某一個 thread 停住,其他 thread 仍然可以繼續完成操作,但不代表每個 thread 都不會等待。
順便介紹 guarantee 常見的三個層級:
3. Lock-Free 的靈魂 —— Memory Ordering
C++ atomic 提供多種 memory order,包含昨天的文章已有提到的 Acquire-Release Semantics。以下再簡單說明幾種最常用的方式:
relaxed:只保證 atomic operation 本身是原子的,不提供跨變數的 ordering。release:發布資料,確保之前的 memory operation 不會被重新排序到 release 之後。acquire:取得資料,確保之後的 memory operation 不會被重新排序到 acquire 之前。seq_cst:提供最強的全域順序保證。典型的 producer-consumer pattern:
std::atomic<bool> ready{false};
Data data;
void producer()
{
data.value = 42;
ready.store(true, std::memory_order_release);
}
void consumer()
{
while (!ready.load(std::memory_order_acquire))
;
std::cout << data.value;
}
這裡的重點不是 ready 本身,而是 release/acquire 建立的 happens-before relationship。Producer 先寫入 data.value,再進行 release store;consumer 透過 acquire load 看到 ready == true 後,就可以安全觀察 producer 在 release 之前完成的寫入。
值得注意的是,ready 這個 atomic operation 並不會建立一般 memory access 的 synchronization,不能單純因為 ready == true 就推論另一個 thread 一定能正確看到 data.value == 42。如果錯誤地改成兩邊都是 relaxed,atomic flag 依然是安全的,但不能因此推導出 data.value 的同步關係。
由此可見,lock-free programming 最危險的部份並非 syntax,而是考驗工程師對 memory model 的理解是否深入。